const std = @import("std");
const debug = std.debug;
const meta = std.meta;
const mem = std.mem;
const fs = std.fs;
const fmt = std.fmt;
const math = std.math;

const Sqlite3 = @import("./sqlite.zig");
const Stmt = Sqlite3.Stmt;
const ui = @import("root").ui;

const UserData: type = @This();

// fields
const row_type: []const Sqlite3.ColumnType = &.{ .int64, .int64, .text, .text, .text };
pub const column_types_zig: type = struct { i64, i64, [:0]const u8, [:0]const u8, [:0]const u8 };
db: Sqlite3,
sql_insert: Sqlite3.Stmt(&.{}),
sql_select_daynumber: Sqlite3.Stmt(row_type),
sql_select_id: Sqlite3.Stmt(&.{.int64}),
sql_delete: Sqlite3.Stmt(&.{}),
sql_record_id: Sqlite3.Stmt(&.{}),
sql_update_time: Sqlite3.Stmt(&.{}),
sql_last_insert_rowid: Sqlite3.Stmt(&.{.int64}),

// function
pub fn init(path: [:0]const u8) !UserData { // <><>
    var db: Sqlite3 = db: { // <+><+> init
        const db: struct { i32, Sqlite3 } = Sqlite3.init(path); // <+><+> struct[1]
        debug.assert(db[0] == Sqlite3.OK);
        errdefer _ = db[1].deinit();
        break :db db[1];
    };
    errdefer db.deinit();

    // create table
    debug.assert(db.exec(
        \\create table if not exists q_a(
        \\    id integer primary key autoincrement,
        \\    day_number integer,
        \\    date text,
        \\    question text,
        \\    answer text
        \\);
    , null, null, null) == Sqlite3.OK);
    // create index
    debug.assert(db.exec("create index if not exists idx_q_a_day_number on q_a(day_number);", null, null, null) == Sqlite3.OK); // day_number
    debug.assert(db.exec("create index if not exists idx_q_a_id on q_a(id);", null, null, null) == Sqlite3.OK); // id
    // create table: unuse_id
    debug.assert(db.exec("create table if not exists unuse_id(id integer primary key);", null, null, null) == Sqlite3.OK);

    // prepare sql
    const sql_insert: Sqlite3.Stmt(&.{}) = Stmt(&.{}).init(&db, "insert into q_a values(?, ?, ?, ?, ?);");
    errdefer sql_insert.deinit();

    const sql_select_daynumber = db.init_stmt(UserData.row_type, "select * from q_a where day_number = ?;");
    errdefer sql_select_daynumber.deinit();

    const sql_select_id: Sqlite3.Stmt(&.{.int64}) = db.init_stmt(&.{.int64}, "select 1 from q_a where id = ?;");
    errdefer sql_select_id.deinit();

    const sql_delete: Sqlite3.Stmt(&.{}) = db.init_stmt(&.{}, "delete from q_a where id = ?;");
    errdefer sql_delete.deinit();

    const sql_record_id: Sqlite3.Stmt(&.{}) = db.init_stmt(&.{}, "insert into unuse_id values(?);");
    errdefer sql_record_id.deinit();

    const sql_update_time: Sqlite3.Stmt(&.{}) = db.init_stmt(&.{}, "update q_a set day_number = ? where id = ?;");
    errdefer sql_update_time.deinit();

    const sql_last_insert_rowid = db.init_stmt(&[_]Sqlite3.ColumnType{.int64}, "select last_insert_rowid() from q_a;");
    errdefer sql_last_insert_rowid.deinit();

    return UserData{
        .db = db,
        .sql_insert = sql_insert,
        .sql_select_daynumber = sql_select_daynumber,
        .sql_select_id = sql_select_id,
        .sql_delete = sql_delete,
        .sql_record_id = sql_record_id,
        .sql_update_time = sql_update_time,
        .sql_last_insert_rowid = sql_last_insert_rowid,
    };
}
pub inline fn deinit(self: *@This()) void {
    defer self.db.deinit();
    defer self.sql_insert.deinit();
    defer self.sql_select_daynumber.deinit();
    defer self.sql_select_id.deinit();
    defer self.sql_delete.deinit();
    defer self.sql_record_id.deinit();
    defer self.sql_update_time.deinit();
    defer self.sql_last_insert_rowid.deinit();
    return;
}
pub inline fn insert(self: *@This(), day_number: i64, date: [:0]const u8, q: [:0]const u8, a: [:0]const u8) !i64 {
    const unuse_id_optional: ?i64 = unuse_id_optional: {
        var unuse_id_optional: ?i64 = null;
        var result = Stmt(&.{.int64}).init(&self.db, "select id from unuse_id;"); // <><> init
        defer result.deinit(); // <><> deinit
        try result.step(&.{}, .{}, &unuse_id_optional, .{ .v = struct {
            fn v(column_values: struct { i64 }, unuse_id_optional_p: *?i64) bool {
                unuse_id_optional_p.* = column_values[0];
                return true;
            }
        }.v });
        break :unuse_id_optional unuse_id_optional;
    };
    if (unuse_id_optional) |unuse_id| {
        _ = delete_unuse_id: {
            var result = Stmt(&.{}).init(&self.db, "delete from unuse_id where id = ?;"); // <><> init
            defer result.deinit(); // <><> defer deinit
            try result.step(&.{.int64}, .{unuse_id}, void, .{});
            break :delete_unuse_id;
        };
        try self.sql_insert.step(&.{ .int64, .int64, .text, .text, .text }, .{ unuse_id, day_number, date, q, a }, void, .{});
    } else {
        try self.sql_insert.step(&.{ .null, .int64, .text, .text, .text }, .{ void{}, day_number, date, q, a }, void, .{});
    }
    return self.get_last_insert_rowid();
}
pub const DeleteErrorResult: type = error{not_found} || Sqlite3.StepErrorResult;
pub inline fn delete(self: *@This(), id: i64) DeleteErrorResult!void {
    var _is_have: bool = false;
    try self.sql_select_id.step(&.{.int64}, .{id}, &_is_have, .{ .v = struct {
        fn func(_: struct { i64 }, is_have: *bool) bool {
            is_have.* = true;
            return true;
        }
    }.func });
    if (!_is_have) return error.not_found;
    try self.sql_delete.step(&.{.int64}, .{id}, void{}, .{});
    try self.sql_record_id.step(&.{.int64}, .{id}, void{}, .{});
    return void{};
}
pub inline fn updateTime(
    self: *@This(),
    id: i64,
    days: i64,
) !void {
    try self.sql_update_time.step(&.{ .int64, .int64 }, .{ days, id }, void{}, .{});
    return void{};
}
pub inline fn get_minDays(const_self: @This()) !i64 {
    var self = const_self; // unsafe
    var stmt_min_days: Sqlite3.Stmt(&.{.int64}) = Stmt(&.{.int64}).init(&self.db, "select MIN(day_number) from q_a;"); // <><> init
    defer stmt_min_days.deinit(); // <><> deinit
    const min_days: i64 = blk: {
        var blk: i64 = undefined; // <><> undefined
        try stmt_min_days.step(
            &.{},
            .{},
            &blk,
            .{ .v = &struct {
                fn func(column_values: struct { i64 }, p_min_days: *i64) bool {
                    p_min_days.* = column_values[0];
                    return true;
                }
            }.func },
        ); // <><> defined
        break :blk blk;
    };
    return min_days;
}
pub inline fn get_last_insert_rowid(self: *@This()) !i64 {
    var last_insert_rowid: i64 = undefined;
    try self.sql_last_insert_rowid.step(&.{}, .{}, &last_insert_rowid, .{
        .v = struct {
            fn v(results: struct { i64 }, p_args: *i64) bool {
                p_args.* = results[0];
                return true;
            }
        }.v,
    });
    return last_insert_rowid;
}
inline fn select_day_number(
    _self: @This(),
    days: i64,
    callback_other_arg: anytype,
    callback: *const fn (
        @This().column_types_zig,
        @TypeOf(callback_other_arg),
    ) bool,
) !void {
    var self = _self;
    try self.sql_select_daynumber.step(
        &.{.int64},
        .{days},
        callback_other_arg,
        .{ .v = callback },
    );
    return void{};
}
inline fn select_id(self: *UserData, id: i64) !bool {
    var is_have: bool = false;
    try self.sql_select_id.step(&.{.int64}, .{id}, &is_have, .{ .v = struct {
        fn v(_: i64, p_is_have: *bool) bool {
            p_is_have.* = true;
            return true;
        }
    } });
    return is_have;
}

pub usingnamespace review; // 复习模块
const review: type = struct {
    /// 获取今天应该复习哪些天, 传入回调函数直接遍历, 免去返回结果
    /// 但是不包含今天
    fn get_reviewDays(
        user_data: UserData,
        today_days: i64,
        callback_other_arg: anytype,
        callback: *const fn (i64, @TypeOf(callback_other_arg)) bool,
    ) void {
        // arg deal
        comptime debug.assert(@typeInfo(@TypeOf(callback_other_arg)) == .Pointer);
        // foreach days
        var idx: u64 = 0;
        const getOffset: fn (usize) i64 = struct { // offset: Offset from tyday_number
            fn func(idx1: usize) i64 {
                const e = std.math.e;
                const @"e^(idx-1)" = std.math.pow(f64, e, @floatFromInt(idx1));
                const @"e-1": f64 = e - 1;
                const @"(e^(idx-1)) / (e-1)": u64 = @intFromFloat(@"e^(idx-1)" / @"e-1");
                return @intCast(@"(e^(idx-1)) / (e-1)" + idx1 + 1);
            }
        }.func;
        const min_days: i64 = user_data.get_minDays() catch @panic("nb");
        while (today_days - getOffset(idx) >= min_days) : (idx += 1) {
            if (callback(today_days - getOffset(idx), callback_other_arg)) break;
        }
        return;
    }
    pub fn reviewPast(p_user_data: UserData, today_days: i64, input: fs.File.Reader) void {
        get_reviewDays(
            p_user_data,
            today_days,
            @as(*ReviewDayKnowledgeOtherArgs, @constCast(&ReviewDayKnowledgeOtherArgs{
                .user_data = p_user_data,
                .input = input,
            })),
            reviewDayKnowledge,
        );

        return;
    }
    // allocator only alloc memory for forget_list
    const ReviewDayKnowledgeOtherArgs: type = struct { user_data: UserData, input: fs.File.Reader };
    fn reviewDayKnowledge(days: i64, p_args: *ReviewDayKnowledgeOtherArgs) bool { // <><>
        var args1: ReviewKnowledgePointArgs = .{ .input = p_args.input };
        p_args.user_data.select_day_number(days, &args1, reviewKnowledgePoint) catch @panic("nb"); // <><> auto
        return false;
    }
    const ReviewKnowledgePointArgs: type = struct { input: fs.File.Reader };
    fn reviewKnowledgePoint(columns: column_types_zig, args: *ReviewKnowledgePointArgs) bool {
        { // print some info
            debug.print("id: {}, ", .{columns[0]});
            debug.print("days: {}, ", .{columns[1]});
            debug.print("date: {s}, \n", .{columns[2]});
            debug.print("question: {s}\n", .{columns[3]});
        }

        const MemoryResult: type = enum {
            remember,
        };

        const memory_result: MemoryResult = ui.get_ui_enum(MemoryResult, args.input);

        switch (memory_result) {
            .remember => {},
        }
        debug.print("answer: {s}\n", .{columns[4]});
        return false;
    }
    pub fn reviewToday(user_data: UserData, today_days: i64, input: fs.File.Reader) void {
        var args: ReviewDayKnowledgeOtherArgs = .{ .user_data = user_data, .input = input };
        _ = @This().reviewDayKnowledge(today_days, &args);
    }
};
